﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Interactivity;
using System.Windows.Markup;
using System.Windows.Media;
using System.Windows.Media.Animation;

namespace Clarity.Prototypes.ListBoxElasticity
{

    public class Elasticity : Behavior<ListBox>
    {
        public static readonly DependencyProperty DurationProperty = DependencyProperty.Register("Duration", typeof(TimeSpan), typeof(Elasticity), new PropertyMetadata(TimeSpan.FromMilliseconds(300), Elasticity.DurationChanged));
        public static readonly DependencyProperty ResistanceProperty = DependencyProperty.Register("Resistance", typeof(double), typeof(Elasticity), new PropertyMetadata(.8));
        public static readonly DependencyProperty EasingFunctionProperty = DependencyProperty.Register("EasingFunction", typeof(EasingFunctionBase), typeof(Elasticity), new PropertyMetadata(new ExponentialEase() { EasingMode = EasingMode.EaseOut, Exponent = 6 }, Elasticity.EaseChanged));
        public static readonly DependencyProperty MinScaleProperty = DependencyProperty.Register("MinScale", typeof(double), typeof(Elasticity), new PropertyMetadata(.7));
        public static readonly DependencyProperty MaxScaleItemsProperty = DependencyProperty.Register("MaxScaleItems", typeof(int), typeof(Elasticity), new PropertyMetadata(8));
        public static readonly DependencyProperty MaxDistanceProperty = DependencyProperty.Register("MaxDistance", typeof(double), typeof(Elasticity), new PropertyMetadata(160.0));

        /// <summary>
        /// Time for list to snap back to it's min/max scrollable position.
        /// </summary>
        public TimeSpan Duration
        {
            get { return (TimeSpan)this.GetValue(Elasticity.DurationProperty); }
            set { this.SetValue(Elasticity.DurationProperty, value); }
        }

        /// <summary>
        /// Amount of resistance to apply to the elasticity when dragging. .5 lets the content move 50% of the actual amount dragged.
        /// </summary>
        public double Resistance
        {
            get { return (double)this.GetValue(Elasticity.ResistanceProperty); }
            set { this.SetValue(Elasticity.ResistanceProperty, value); }
        }

        /// <summary>
        /// The easing to apply to the snap back animation.
        /// </summary>
        public EasingFunctionBase EasingFunction
        {
            get { return (EasingFunctionBase)this.GetValue(Elasticity.EasingFunctionProperty); }
            set { this.SetValue(Elasticity.EasingFunctionProperty, value); }
        }

        /// <summary>
        /// The min amount an element is squished when dragging past the boundary. .5 means an element will be 50% smaller if you drag the maximum allowed.
        /// </summary>
        public double MinScale
        {
            get { return (double)this.GetValue(Elasticity.MinScaleProperty); }
            set { this.SetValue(Elasticity.MinScaleProperty, value); }
        }

        /// <summary>
        /// Number of items to scale.  For lists with larger items you can lower this number.
        /// </summary>
        public int MaxScaleItems
        {
            get { return (int)this.GetValue(Elasticity.MaxScaleItemsProperty); }
            set { this.SetValue(Elasticity.MaxScaleItemsProperty, value); }
        }

        /// <summary>
        /// Maximum distance to overscroll
        /// </summary>
        public double MaxDistance
        {
            get { return (double)this.GetValue(Elasticity.MaxDistanceProperty); }
            set { this.SetValue(Elasticity.MaxDistanceProperty, value); }
        }

        private static void DurationChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
        {
            ((Elasticity)sender).Duration = (TimeSpan)e.NewValue;
        }

        private static void EaseChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
        {
            ((Elasticity)sender).EasingFunction = (EasingFunctionBase)e.NewValue;
        }

        private static string storyboardXaml = @"
        <Storyboard xmlns=""http://schemas.microsoft.com/winfx/2006/xaml/presentation"">
            <DoubleAnimation Duration=""0:0:0.3"" To=""0"" Storyboard.TargetProperty=""(UIElement.RenderTransform).(CompositeTransform.TranslateY)"">
              <DoubleAnimation.EasingFunction>
                                  <ExponentialEase EasingMode=""EaseOut"" Exponent=""6"" />
                           </DoubleAnimation.EasingFunction>
            </DoubleAnimation>
        </Storyboard>";

        private ScrollViewer _scrollViewer;
        private ScrollContentPresenter _scp;
        private Storyboard _storyboard;
        private bool _needsSpringback;
        private FrameworkElement _manipulatableElement;
        private Panel _itemsHost;
        private IEnumerable<FrameworkElement> _items;
        private CompositeTransform _itemTransform;
        private CompositeTransform _contentTransfrom;
        private Point _origin = new Point(.5, 1);
        private bool isLoaded = false;

        protected override void OnAttached()
        {
            base.OnAttached();

            this.AssociatedObject.Loaded += new RoutedEventHandler(ListBoxLoaded);
            this.AssociatedObject.SizeChanged += new SizeChangedEventHandler(ListBoxSizeChanged);

            _storyboard = XamlReader.Load(storyboardXaml) as Storyboard;
        }

        void ListBoxSizeChanged(object sender, SizeChangedEventArgs e)
        {
            Rect bounds = new Rect(0, 0, AssociatedObject.ActualWidth, AssociatedObject.ActualHeight);
            AssociatedObject.Clip = new RectangleGeometry() { Rect = bounds };
        }

        protected override void OnDetaching()
        {
            base.OnDetaching();

            AssociatedObject.Loaded -= new RoutedEventHandler(ListBoxLoaded);
            AssociatedObject.SizeChanged -= new SizeChangedEventHandler(ListBoxSizeChanged);
            _manipulatableElement.ManipulationDelta -= new EventHandler<ManipulationDeltaEventArgs>(ManipulationDelta);
            _manipulatableElement.ManipulationCompleted -= new EventHandler<ManipulationCompletedEventArgs>(ManipulationCompleted);
            _manipulatableElement.ManipulationStarted -= new EventHandler<ManipulationStartedEventArgs>(ManipulationStarted);
        }

        void ManipulationStarted(object sender, ManipulationStartedEventArgs e)
        {
            _itemTransform = new CompositeTransform() { ScaleY = 1 };
            _contentTransfrom = new CompositeTransform();
            _scp.RenderTransform = _contentTransfrom;
        }

        void ListBoxLoaded(object sender, RoutedEventArgs e)
        {
            _scrollViewer = this.AssociatedObject.GetVisualChildren().OfType<ScrollViewer>().First();
            _manipulatableElement = _scrollViewer.GetVisualChild(0) as FrameworkElement;
            _scp = _scrollViewer.GetVisualDescendents().OfType<ScrollContentPresenter>().First();

            if (_scrollViewer != null && _manipulatableElement != null && !isLoaded)
            {
                DoubleAnimation da = _storyboard.Children[0] as DoubleAnimation;
                object t = Storyboard.TargetPropertyProperty;
                Storyboard.SetTarget((_storyboard.Children[0] as DoubleAnimation), _scp);
                
                da.Duration = Duration;
                da.EasingFunction = EasingFunction;
                _manipulatableElement.ManipulationDelta += new EventHandler<ManipulationDeltaEventArgs>(ManipulationDelta);
                _manipulatableElement.ManipulationCompleted += new EventHandler<ManipulationCompletedEventArgs>(ManipulationCompleted);
                _manipulatableElement.ManipulationStarted += new EventHandler<ManipulationStartedEventArgs>(ManipulationStarted);
                isLoaded = true;
            }
        }

        void ManipulationDelta(object sender, ManipulationDeltaEventArgs e)
        {
            if ((_scrollViewer.VerticalOffset == 0 && e.CumulativeManipulation.Translation.Y > 1) ||
                (_scrollViewer.VerticalOffset == _scrollViewer.ScrollableHeight && e.CumulativeManipulation.Translation.Y < 1))
            {
                e.Handled = true;

                if (Math.Abs(e.CumulativeManipulation.Translation.Y) > MaxDistance)
                    return;

                _contentTransfrom.TranslateY += e.DeltaManipulation.Translation.Y * Resistance;
                double scale = GetScale(Math.Abs(e.CumulativeManipulation.Translation.Y));
                _contentTransfrom.ScaleY = scale;
                _needsSpringback = true;
            }
        }

        void ManipulationCompleted(object sender, ManipulationCompletedEventArgs e)
        {
            if (_needsSpringback)
            {
                _needsSpringback = false;
                _contentTransfrom.ScaleY = 1;
                _storyboard.Begin();
                _items = null;
            }
        }

        private double GetScale(double distance)
        {
            if (distance > 0)
            {
                var ratio = distance / this.AssociatedObject.ActualHeight;
                return (1 - ((1 - MinScale) * ratio));
            }

            return 1;
        }

        internal Panel ItemsHost
        {
            get
            {
                // Lookup the ItemsHost if we haven't already cached it.
                if (_itemsHost == null && AssociatedObject != null && AssociatedObject.ItemContainerGenerator != null)
                {
                    // Get any live container
                    DependencyObject container = AssociatedObject.ItemContainerGenerator.ContainerFromIndex(0);

                    if (container != null)
                    {
                        // Get the parent of the container
                        _itemsHost = VisualTreeHelper.GetParent(container) as Panel;
                    }
                }

                return _itemsHost;
            }
        }
    }

    


    public static class Extensions
    {
        public static void ForEach<T>(this IEnumerable<T> source, Action<T> action)
        {
            foreach (var item in source)
                action(item);
        }

        public static FrameworkElement GetVisualChild(this FrameworkElement node, int index)
        {
            return VisualTreeHelper.GetChild(node, index) as FrameworkElement;
        }

        public static IEnumerable<FrameworkElement> GetVisualChildren(this FrameworkElement root)
        {
            for (int i = 0; i < VisualTreeHelper.GetChildrenCount(root); i++)
                yield return VisualTreeHelper.GetChild(root, i) as FrameworkElement;
        }

        public static IEnumerable<FrameworkElement> GetVisualDescendents(this FrameworkElement root)
        {
            Queue<IEnumerable<FrameworkElement>> toDo = new Queue<IEnumerable<FrameworkElement>>();

            toDo.Enqueue(root.GetVisualChildren());
            while (toDo.Count > 0)
            {
                IEnumerable<FrameworkElement> children = toDo.Dequeue();
                foreach (FrameworkElement child in children)
                {
                    yield return child;
                    toDo.Enqueue(child.GetVisualChildren());
                }
            }
        }
    }
}
